/* ***** BEGIN LICENSE BLOCK *****
 *   Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 * 
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is JsWs.
 *
 * The Initial Developer of the Original Code is
 * Chris Winberry.
 * Portions created by the Initial Developer are Copyright (C) 2007
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 * 
 * ***** END LICENSE BLOCK ***** */
// netscape.security.PrivilegeManager.enablePrivilege("UniversalPreferencesRead UniversalPreferencesWrite UniversalBrowserRead UniversalBrowserWrite UniversalXPConnect")

/**
 * @class Socket listener and host to all server scope varaibles and objects
 * @constructor
 */
function JsWsListener () {
	/**
	 * Hash to store server scoped data for sharing between requests and pages
	 * @type Hash
	 */
	this.state = new Array();
	/** @private */
	this.modules = new Array();
	/**
	 * Path to the server's root
	 * @type nsILocalFile
	 */
	this.root = Cc["@mozilla.org/file/directory_service;1"].
		getService(Ci.nsIProperties).
		get('ProfD', Ci.nsILocalFile);
	/**
	 * Path to the server's document root
	 * @type nsILocalFile
	 */
	this.serverRoot = this.root.clone();
	this.tools.appendPath(this.serverRoot, JSWS_ROOT);

	this.documentRoot = this.serverRoot.clone();
	this.tools.appendPath(this.documentRoot, JSWS_DOCROOT);

//	this.tools.appendPath(this.root, JSWS_ROOT);

//	if (!this.modules || this.modules.length < 1)
	this.loadModules();

	for (var key in this.modules) {
//		if (this.modules[key][JSWS_MODULES_INITPAGEKEY]) {
		if (this.modules[key].initPage) {
			var mappedModule = this.modules[key];
			var handlerObj = {
					"write" : function () {},
					"destroy" : function () {}
					}
			var request = new Request();
//			request.path = mappedModule[JSWS_MODULES_VIRTUALPATHKEY]
//				+ mappedModule[JSWS_MODULES_INITPAGEKEY]
			request.path = mappedModule.virtualPath
				+ mappedModule.initPage;
			var response = new Response(handlerObj);
			var script = new ScriptContext(this, request.path);
			var codeHost = new EvalCodeHost(this, request, response, script);

			try {
				codeHost.require(
//					mappedModule[JSWS_MODULES_VIRTUALPATHKEY] +
//					mappedModule[JSWS_MODULES_INITPAGEKEY]
					mappedModule.virtualPath +
					mappedModule.initPage
					);
//				JsWs.prototype.log("Module \"" + mappedModule[JSWS_MODULES_NAMEKEY] + "\" initialized");
				JsWs.prototype.log("Module \"" + mappedModule.name + "\" initialized");
			} catch (ex) {
//				JsWs.prototype.log("Init page for module \"" + mappedModule[JSWS_MODULES_NAMEKEY] + "\" failed : " + ex);
				JsWs.prototype.log("Init page for module \"" + mappedModule.name + "\" failed : " + ex);
			} finally {
				request.destroy();
				request = null;
				response.destroy();
				response = null;
				script.destroy();
				script = null;
				codeHost.destroy();
				codeHost = null;
			}
		}
	}
}


/**
 * Runs cleanup on internal objects
 */
JsWsListener.prototype.destroy = function () {
	this.state = null;
	this.modules = null;
	this.root = null;
	this.documentRoot = null;
}


/** @private */
JsWsListener.prototype.loadModules = function () {
	if (this.modules && this.modules.length > 0)
		return;
	/*
	try {
		var extManager = Cc["@mozilla.org/extensions/manager;1"]
			.createInstance(Ci.nsIExtensionManager);
		var prefSvc = Cc["@mozilla.org/preferences-service;1"]
			.getService(Ci.nsIPrefService);
		var prefBranch = prefSvc.getBranch(JSWS_PREF_MODULES_ROOT);
		var children = prefBranch.getChildList("", {});
		for (var i = 0; i < children.length; i++) {
			var child = children[i];
			var parts = child.split('.');
			var module = parts.shift();
			var key = parts.join('.');
			if (!this.modules[module])
				this.modules[module] = new Array();
			this.modules[module][key] = prefBranch.getCharPref(child);
			if (key == JSWS_MODULES_IDKEY) {
				this.modules[module][JSWS_MODULES_EXTENSIONPATHKEY] =
					extManager
					.getInstallLocation(this.modules[module][key])
					.getItemLocation(this.modules[module][key])
					.path;
			}
		}
	} catch (e) { }
	*/
	this.modules = Module.loadFromPrefs(JSWS_PREF_MODULES_ROOT);

	/*
	this.modules[JSWS_MODULES_ROOTKEY] = new Array();
	this.modules[JSWS_MODULES_ROOTKEY][JSWS_MODULES_IDKEY] = JSWS_MODULES_ROOTKEY;
	this.modules[JSWS_MODULES_ROOTKEY][JSWS_MODULES_NAMEKEY] = JSWS_MODULES_ROOTKEY;
	this.modules[JSWS_MODULES_ROOTKEY][JSWS_MODULES_EXTENSIONPATHKEY] = this.serverRoot.path;
	this.modules[JSWS_MODULES_ROOTKEY][JSWS_MODULES_CONTENTPATHKEY] = JSWS_DOCROOT;
	this.modules[JSWS_MODULES_ROOTKEY][JSWS_MODULES_VIRTUALPATHKEY] = "/";
	this.modules[JSWS_MODULES_ROOTKEY][JSWS_MODULES_HANDLERKEY] = "/../pageHandler." +  JSWS_SCRIPT_FILEEXT;
	this.modules[JSWS_MODULES_ROOTKEY][JSWS_MODULES_STARTPAGEKEY] = "/index." +  JSWS_SCRIPT_FILEEXT;
	*/
	var module = new Module(JSWS_MODULES_ROOTKEY);
	module.setProperty(JSWS_MODULES_NAMEKEY, JSWS_MODULES_ROOTKEY);
	module.setProperty(JSWS_MODULES_EXTENSIONPATHKEY, this.serverRoot.path);
	module.setProperty(JSWS_MODULES_CONTENTPATHKEY, JSWS_DOCROOT);
	module.setProperty(JSWS_MODULES_VIRTUALPATHKEY, "/");
	module.setProperty(JSWS_MODULES_HANDLERKEY, "/../pageHandler." +  JSWS_SCRIPT_FILEEXT);
	module.setProperty(JSWS_MODULES_STARTPAGEKEY, "/index." +  JSWS_SCRIPT_FILEEXT);
	this.modules[JSWS_MODULES_ROOTKEY] = module;
}

/**
 * Maps a path to the appropriate module
 * @param {String} path Absolute server path to find the mapped module for
 * @returns Module that handles the path
 * @type Module
 */
JsWsListener.prototype.mapModule = function (path) {
	if ((typeof path == "string") || (path instanceof String))
		path = new VirtualPath(path);
	if (!(path instanceof VirtualPath))
		throw(new ExParamError("Parameter \"path\" is of an unexpected type (" + typeof path + ")", 0, null, arguments));
	//path = this.tools.normalizePath(path);

	var mappedModule = this.modules[JSWS_MODULES_ROOTKEY];
	for (var key in this.modules) {
		if (key == JSWS_MODULES_ROOTKEY)
			continue;
//		if (path.path.indexOf(this.modules[key][JSWS_MODULES_VIRTUALPATHKEY]) == 0)
		if (path.path.indexOf(this.modules[key].virtualPath) == 0)
//			if (this.modules[key][JSWS_MODULES_VIRTUALPATHKEY].length > mappedModule[JSWS_MODULES_VIRTUALPATHKEY].length)
			if (this.modules[key].virtualPath.length > mappedModule.virtualPath.length)
				mappedModule = this.modules[key];
	}

	return(mappedModule);
}

/** @private */
JsWsListener.prototype.onSocketAccepted = function (socket, transport) {
	new JsWsConnectionHandler(this, socket, transport);
}


/** @private */
JsWsListener.prototype.onStopListening = function (socket, status) {}

/** TODO */
JsWsListener.prototype.tools = {

	//"normalizePath": function (path) {
	//		return(path.replace(/(\/|\\)/g, VirtualPath.sysSeparator));
	//	},

	"htmlEscape": function (sContent) {
			return(sContent.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\n/g, "<br>\n"));
		},

	//"pathSeparator": Cc["@mozilla.org/file/directory_service;1"].
	//	getService(Ci.nsIProperties).
	//	get('ProfD', Ci.nsILocalFile).
	//	path.match('/') ? '/' : '\\',

	"appendPath": function (oFile, sPath) {
		var oLocalFile = oFile.QueryInterface(Ci.nsILocalFile);
		var pathParts = sPath.split(/(?:\/|\\)+/);
		for (var i = 0; i < pathParts.length; i++) {
			if (pathParts[i] == '.') { continue; }
			if (pathParts[i] == '..') {
				//oLocalFile.initWithPath(oLocalFile.parent.path);
				oLocalFile = oLocalFile.parent;
			} else {
				oLocalFile.append(pathParts[i]);
			}
		}
		oLocalFile.normalize();
	},

	"enumTypes": function (sType, oTarget) {
		var objs = new Array();
		if (oTarget == null) { oTarget = window; }

		for (var i in oTarget) {
			try {
				if (sType == null | typeof oTarget[i] == sType) {
					objs[i] = typeof oTarget[i];
				}
			} catch (e) {
				// Hmmm
			}
		}
		return(objs);
	},

	"enumFunctions": function (oTarget) {return(enumTypes("function", oTarget)); },

	"enumStrings": function (oTarget) { return(enumTypes("string", oTarget)); },

	"enumObjects": function (oTarget) { return(enumTypes("object", oTarget)); },

	"enumNumbers": function (oTarget) { return(enumTypes("number", oTarget)); },

	"enumBools": function (oTarget) { return(enumTypes("boolean", oTarget)); }

}

/********************************************************************************
 * JsWs
 *******************************************************************************/
/**
 * @class JsWs server object. Exposes high-level starting and stopping of the server
 * @contructor
 * @param {Number} port Port to listen on
 * @param {Boolean} localOnly Flag indicating whether to ignore requests from remote addresses
 * @param {Boolean} threaded Flag indicating whether the server should run in thread separate
 *  from the main JS/GUI thread
 */
function JsWs (port, localOnly, threaded) {
	/** @private */
	this.port = port;
	/** @private */
	this.localOnly = !!localOnly;
	/** @private */
	this.isThreaded = !!threaded;
}


/** @private */
JsWs.prototype.QueryInterface = function (aIID) {
	if(
		!aIID.equals(Ci.nsISupports) &&
		!aIID.equals(Ci.nsIRunnable) &&
		!aIID.equals(Ci.nsIClassInfo)
		)
		throw Components.results.NS_ERROR_NO_INTERFACE;

	return this;
}


/** Starts the server listening */
JsWs.prototype.start = function () {
	/* Something wierd is going on with the math functions.
	 * Looks like FF has major problems if the math library is
	 * called/loaded for the first time by something other
	 * than the main JS thread. Hence the stupidness that follows...
	 * KLUDGE ZONE */
	parseInt("1");
	new Number(1);
	/* KLUDGE ZONE */

	var currentThread = Cc["@mozilla.org/thread;1"]
		.createInstance(Ci.nsIThread)
		.currentThread;

	this.log("Server Starting [port:" + this.port + "]");
	this.eqs = Cc["@mozilla.org/event-queue-service;1"]
		.getService(Ci.nsIEventQueueService);
	this.server = Cc["@mozilla.org/network/server-socket;1"]
		.createInstance(Ci.nsIServerSocket);
	this.server.init(this.port, this.localOnly, -1);
	this.listener = new JsWsListener();
//	currentThread.sleep(200);
	if (this.isThreaded) {
		this.thread = Cc["@mozilla.org/thread;1"]
			.createInstance(Ci.nsIThread);
		this.thread.init(
			this,
			0,
			Ci.nsIThread.PRIORITY_LOW,
			Ci.nsIThread.SCOPE_LOCAL,
			Ci.nsIThread.STATE_UNJOINABLE
			);
	}
	else {
		this.server.asyncListen(this.listener);
	}
	this.log("Server Started");
}


/**
 * Runs cleanup on internal objects
 */
JsWs.prototype.destroy = function () {
	this.stop();
	this.eqs = null;
	this.server = null;
	if (this.listener)
		this.listener.destroy();
	this.listener = null;
}


/** Stops the server listening */
JsWs.prototype.stop = function () {
	this.log("Stopping...");
	if (this.isThreaded && this.thread)
		this.thread.interrupt();
	if (this.server)
		this.server.close();
	this.log("done");
}


/** @private */
JsWs.prototype.run = function () {
	this.eq = this.eqs.createFromIThread(this.thread, false);
//	this.thread.sleep(300);
	this.server.asyncListen(this.listener);
//	this.thread.sleep(300);
	this.eq.eventLoop();
	this.eq.processPendingEvents();
	this.eq.stopAcceptingEvents();
	this.eq.processPendingEvents();
	this.eqs.destroyThreadEventQueue();
}


/**
 * Logs a message to the JavaScript console
 * @param {String} s Message to log
 */
JsWs.log = function (s) {
	var currentThread = Cc["@mozilla.org/thread;1"]
		.createInstance(Ci.nsIThread)
		.currentThread;
	Cc["@mozilla.org/consoleservice;1"].
		getService(Ci.nsIConsoleService).
		logStringMessage("(" + currentThread.priority + ")[JsWs] " + s);
}
JsWs.prototype.log = JsWs.log;

/** @private */
JsWs.prototype.getInterfaces = function (count) {
	var ifaces = [
		Ci.nsISupports,
		Ci.nsIRunnable,
		Ci.nsIClassInfo
		];
	count.value = ifaces.length;
	return ifaces;
}


/** @private */
JsWs.prototype.getHelperForLanguage = function (language) {
	return null;
}

// TODO: smart handling of string vs. nsIFile paths
